using System;
using System.Collections;
using System.Diagnostics;
using System.ComponentModel;
using System.Collections.Generic;
using System.Threading;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Design;
using System.Text;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Windows.Forms;
using BrainTechLLC.Communications;
using AcedUtils;

// This code file contains functions written by Owen Emlen for use
// by Owen Emlen, BrainTech LLC, and contract clients.  Functions used
// within this code file do not become the exclusive property of any
// one contracting client (i.e. I have, and will continue to use these 
// general functions in projects with other clients).  However, with that said, 
// clients may modify and use this code as they see fit.  Although not 
// legally enforcable, I would appreciate if clients refrain from 
// copying/pasting this code into applications other than the primary project 
// for which my work was contracted.  If you do end up using these functions 
// in other applications, I would appreciate a brief mention in a comment 
// somewhere within the code file, along with contact information so that 
// those who appreciate the code might contact me regarding future 
// programming contracts or services.  Thank you!
// Owen Emlen, owene_1998@yahoo.com, http://www.braintechllc.com/owen.aspx
namespace BrainTechLLC.Communications
{
  public enum SupportedCommunicationsProtocol
  {
    TCPIP = 0,
    HTTP = 1
  }

  public enum HostNegotiationStatus
  {
    None = 0,
    RefusedToHost = 1,
    AcceptedHosting = 2
  }

  /// <summary>
  /// Basic implementation of the ICommunications interface.  P2P and Web Communications libraries
  /// both inherit from this base class
  /// </summary>
  [Browsable(true)]
  public class CommunicationsLibrary : ICommunications
  {
    /// <summary>
    /// P2P library uses both incoming and outgoing connections
    /// </summary>
    private CommConnection outgoingconnection = null;
    public static BinaryWriter bwSessionRecorder = null;
    public static BinaryReader brSessionPlayback = null;
    public static bool fPausePlayback = false;
    public static FileStream fsRecord = null;
    public static FileStream fsPlayback = null;
    public static bool fStopPlayback = false;
    /// <summary>
    /// Handle 256K chunks of data at a time
    /// </summary>
    public static int MaxRecvLen = 256000;
    /// <summary>
    /// If true, then the communications layer is responsible for requesting a full (remote) screen refresh
    /// </summary>
    public bool fWantRefresh = false;
    public HostNegotiationStatus hostReqStatus = HostNegotiationStatus.None;
    public HostNegotiationStatus hostRespStatus = HostNegotiationStatus.None;
    private AcedCompressionLevel compressionLevel = AcedCompressionLevel.Maximum;
    
    [Category("[Custom]"), DisplayName("Byte compression"),
    Description("The compression level determines how thoroughly bitmaps should be compressed (non-lossy).  Obviously, Maximum is optimal for bandwidth if you have a fast CPU.")]
    public AcedCompressionLevel CompressionLevel { get { return compressionLevel; } set { compressionLevel = value; } }

    public void SetHostNegotiationStatus(HostNegotiationStatus status) { hostReqStatus = status; }
    public void SetHostResponseStatus(HostNegotiationStatus status) { hostRespStatus = status; }

    /// <summary>
    /// Compress an audio packet for transmission to another BrainTechLLC.Communications client
    /// </summary>
    /// <param name="rgbUncompressed"></param>
    /// <param name="nDataLength"></param>
    /// <param name="rgbCompressedAudio"></param>
    /// <returns></returns>
    public int CompressAudioPacket(byte[] rgbUncompressed, int nDataLength, out byte[] rgbCompressedAudio)
    {
      rgbCompressedAudio = new byte[nDataLength + 4];
      Monitor.Enter(AcedUtils.AcedDeflator.Instance);
      int nNewLen = AcedDeflator.Instance.Compress(
        rgbUncompressed, 0,
        nDataLength,
        compressionLevel,
        rgbCompressedAudio, 0
        );
      Monitor.Exit(AcedUtils.AcedDeflator.Instance);
      return nNewLen;
    }

    /// <summary>
    /// Decompress an audio packet coming from another BrainTechLLC.Communications client
    /// </summary>
    /// <param name="rgbCompressed"></param>
    /// <param name="nDataLength"></param>
    /// <param name="rgbDecompressedAudio"></param>
    /// <returns></returns>
    public int DecompressAudioPacket(byte[] rgbCompressed, int nDataLength, out byte[] rgbDecompressedAudio)
    {
      Monitor.Enter(AcedUtils.AcedInflator.Instance);
      int nDecompressedLength = AcedInflator.GetDecompressedLength(rgbCompressed, 0);
      rgbDecompressedAudio = AcedInflator.Instance.Decompress(rgbCompressed, 0, 0, 0);
      Monitor.Exit(AcedUtils.AcedInflator.Instance);
      if (rgbDecompressedAudio != null)
      {
        return nDecompressedLength;
      }
      return 0;
    }

    /// <summary>
    /// (used in Owen's audio/video conferencing, motion detection, and sound detection software)
    /// </summary>
    /// <returns></returns>
    public virtual MemoryStream GetPendingAudioPacket()
    {
      if (fInitialized && ConnectionHub != null)
      {
        for (int n = 0; n < ConnectionHub.rgDescriptors.Count; n++)
        {
          CommConnection cn = ConnectionHub.rgDescriptors[n];
          if (cn.NeedsClosing == false && cn.rgmsAudio != null &&
            cn.rgmsAudio.Count > 0)
          {
            MemoryStream msAudio = (MemoryStream)cn.rgmsAudio.Dequeue();
            msAudio.Seek(0, SeekOrigin.Begin);
            byte[] rgAudioBuffer = null;
            int nPacketLength = DecompressAudioPacket(
              msAudio.GetBuffer(), (int)msAudio.Length, out rgAudioBuffer);
            if (nPacketLength > 0)
            {
              msAudio.Close();
              MemoryStream msFullAudio = new MemoryStream(nPacketLength);
              BinaryWriter bw = new BinaryWriter(msFullAudio);
              bw.Write(rgAudioBuffer, 0, nPacketLength);
              msFullAudio.Seek(0, SeekOrigin.Begin);
              return msFullAudio;
            }
          }
        }
      }

      return null;
    }

    /// <summary>
    /// Retrieves raw packet data from the playback stream that can be used to "play back" a recorded
    /// session.  This data is a near replica of what is transmitted over the network during the actual
    /// session, which allows the same code to be used to "watch" a remote session, or "play back" a
    /// recorded session.
    /// </summary>
    /// <returns></returns>
    public byte[] GetPlaybackData()
    {
      if (brSessionPlayback == null) return null;
      // Check if the user requested playback to be stopped
      if (fStopPlayback == true)
      {
        fStopPlayback = false;
        MessageBox.Show("Playback stopped");
        brSessionPlayback.Close();
        if (fsPlayback != null)
        {
          try { fsPlayback.Close(); }
          catch (Exception ex) { Debug.WriteLine(ex.ToString()); }
        }
        brSessionPlayback = null;
        fPausePlayback = false;
        return null;
      }

      // If playback is paused, return no data (null) but do not otherwise indicate that playback has stopped
      if (fPausePlayback == true) return null;
      byte[] rgNewData = new byte[MaxRecvLen];
      int nRead = brSessionPlayback.Read(rgNewData, 0, MaxRecvLen);
      if (nRead == 0)
      {
        // If we are at the end of playback...
        while (outgoingConnection.rgbmIncoming.Count > 0 || outgoingConnection.rgIncomingBytes.Count > 0 ||
          outgoingConnection.qCommandsToExecute.Count > 0 || outgoingConnection.rgmsIncomingCommands.Count > 0)
        {
          // Wait for the final few images/updates to play back
          Thread.Sleep(10);
        }
        MessageBox.Show("Session playback complete");        
        try
        {
          brSessionPlayback.Close();
          fsPlayback.Close();
        }
        catch (Exception ex) { Debug.WriteLine(ex.ToString()); }
        brSessionPlayback = null;
        fPausePlayback = false;
        fStopPlayback = false;
        return null;
      }
      else if (nRead < MaxRecvLen)
      {
        // There is data to played back, but not MaxRecvLen bytes.  Create a smaller array to return.
        byte[] rgLessData = new byte[nRead];
        Array.Copy(rgNewData, 0, rgLessData, 0, nRead);
        return rgLessData;
      }

      return rgNewData;
    }

    /// <summary>
    /// Feeds playback data into the "incoming" data stream
    /// </summary>
    /// <param name="rgPlaybackData"></param>
    public void EnqueuePlaybackData(byte[] rgPlaybackData)
    {
      while (outgoingConnection.rgmsIncomingCommands.Count > 0 ||
        outgoingConnection.rgbmIncoming.Count > 0 || outgoingConnection.qCommandsToExecute.Count > 0)
      {
        Thread.Sleep(20);
        Application.DoEvents();
      }
      Monitor.Enter(outgoingConnection.rgIncomingBytes);
      outgoingConnection.rgIncomingBytes.Enqueue(rgPlaybackData);
      Monitor.Exit(outgoingConnection.rgIncomingBytes);
    }

    public bool PlayingBackData { get { return (brSessionPlayback != null); } }

    public void WriteoutgoingData(int nMessageSize, byte[] rgBytes, int nLen)
    {
      if (CommunicationsLibrary.bwSessionRecorder == null) return;
      DateTime dt = DateTime.Now;
      lock (CommunicationsLibrary.bwSessionRecorder)
      {
        CommunicationsLibrary.bwSessionRecorder.Write(dt.ToBinary());
        CommunicationsLibrary.bwSessionRecorder.Write(nMessageSize);
        CommunicationsLibrary.bwSessionRecorder.Write(rgBytes, 0, nLen);
      }
    }

    public void WriteIncomingData(int nMessageSize, byte[] rgBytes, int nLen)
    {
      if (CommunicationsLibrary.bwSessionRecorder == null) return;
      DateTime dt = DateTime.Now;
      lock (CommunicationsLibrary.bwSessionRecorder)
      {
        CommunicationsLibrary.bwSessionRecorder.Write(dt.ToBinary());
        CommunicationsLibrary.bwSessionRecorder.Write(nMessageSize);
        CommunicationsLibrary.bwSessionRecorder.Write(rgBytes, 0, nLen);
      }
    }

    public static void StopRecordingSession(FileStream fs)
    {
      lock (CommunicationsLibrary.bwSessionRecorder)
      {
        CommunicationsLibrary.bwSessionRecorder.Flush();
        CommunicationsLibrary.bwSessionRecorder.Close();
        fs.Close();
        CommunicationsLibrary.bwSessionRecorder = null;
      }
    }

    [Category("[Custom]"), TypeConverter(typeof(ExpandableObjectConverter))]
    public CommConnection outgoingConnection
    {
      get { return outgoingconnection; }
      set { outgoingconnection = value; }
    }
    private CommConnection mainlistener = null;

    [Category("[Custom]"), TypeConverter(typeof(ExpandableObjectConverter))]
    public CommConnection ConnectionHub
    {
      get { return mainlistener; }
      set { mainlistener = value; }
    }

    protected bool fInitialized = false;
    protected int nUserIdentity;    
    protected int nGroupID;
    protected string sUserAuthenticationToken;
    protected string sUserName;
    public bool fNewConnection = false;

    public bool InitializedAndActive { get { return fInitialized; } }
    public virtual void GracefulShutdown() { ConnectionHub.Stop = true; }

    public virtual void TerminateOutgoingConnection()
    {
      if (outgoingConnection == null) return;
      if (outgoingConnection == ConnectionHub) return;
      outgoingConnection.NeedsClosing = true;
      outgoingConnection = null;
    }

    /// <summary>
    /// Retrieves a bitmap that has been retrieved / sent to this client, or null if no incoming bitmaps are pending
    /// </summary>
    /// <returns></returns>
    public virtual Bitmap GetIncomingImage()
    {
      Bitmap bmNew = null;
      if (fInitialized && ConnectionHub != null)
      {
        for (int n = 0; n < ConnectionHub.rgDescriptors.Count; n++)
        {
          try
          {
            CommConnection cn = ConnectionHub.rgDescriptors[n];
            if (cn.NeedsClosing == false && cn.rgbmIncoming != null &&
              cn.rgbmIncoming.Count > 0)
            {
              bmNew = (Bitmap)cn.rgbmIncoming.Dequeue();
              break;
            }
          }
          catch { }
        }
      }
      return bmNew;
    }

    private int nNewWidth = -1;
    private int nNewHeight = -1;
    private bool fWantsFullUpdate = false;
    public bool CheckWantsFullUpdate()
    {
      if (fWantsFullUpdate)
      {
        fWantsFullUpdate = false;
        return true;
      }
      return false;
    }

    /// <summary>
    /// Determines if the bitmap size / screen resolution has changed
    /// </summary>
    /// <param name="Width"></param>
    /// <param name="Height"></param>
    /// <returns></returns>
    public bool CheckResolutionChanged(ref int Width, ref int Height)
    {
      if (nNewWidth != Width || nNewHeight != Height)
      {
        Width = nNewWidth;
        Height = nNewHeight;
        return true;
      }
      return false;
    }

    /// <summary>
    /// Handles screen update packets, resolution change packets, and remote control execution instructions
    /// </summary>
    /// <param name="fReceivedCommand"></param>
    /// <returns></returns>
    public virtual ScreenUpdateDataPacket GetPendingUpdatePacket(ref bool fReceivedCommand)
    {
      ScreenUpdateDataPacket screenUpdatePacket = null;

      if (fInitialized && ConnectionHub != null)
      {
        for (int n = 0; n < ConnectionHub.rgDescriptors.Count; n++)
        {
          CommConnection cn = ConnectionHub.rgDescriptors[n];
          if (cn.NeedsClosing == false && cn.rgmsIncomingCommands != null &&
            cn.rgmsIncomingCommands.Count > 0)
          {
            MemoryStream msCommand = (MemoryStream)cn.rgmsIncomingCommands.Dequeue();
            msCommand.Seek(0, SeekOrigin.Begin);

            BinaryReader br = new BinaryReader(msCommand);
            short cmdType = (short)br.ReadInt16();
            if (cmdType == (short)MessageTypes.CommandExecutionPacket)
            {
              int nCommandLength = (int)msCommand.Length - 2;
              MemoryStream msCommandsExeToQueue = new MemoryStream(nCommandLength);
              msCommandsExeToQueue.Write(br.ReadBytes(nCommandLength), 0, nCommandLength);
              msCommandsExeToQueue.Seek(0, SeekOrigin.Begin);
              ConnectionHub.qCommandsToExecute.Enqueue(msCommandsExeToQueue);
              fReceivedCommand = true;
            }
            else if (cmdType == (short)MessageTypes.HostResolutionChange)
            {
              // This is a screen resolution change
              nNewWidth = (int)(br.ReadInt16());
              nNewHeight = (int)(br.ReadInt16());
              fReceivedCommand = true;
            }
            else if (cmdType == (short)MessageTypes.ScreenUpdatePacket)
            {
              // This is a screen update command
              int nUpdates = (int)(br.ReadInt16());

              List<ScreenUpdateData> listOfScreenUpdates = new List<ScreenUpdateData>(nUpdates);
              List<Rectangle> rectUpdates = new List<Rectangle>(nUpdates);
              List<int> rgByteLengths = new List<int>(nUpdates);

              for (int nUpdate = 0; nUpdate < nUpdates; nUpdate++)
              {
                int nLeft = (int)(br.ReadInt16());
                int nTop = (int)(br.ReadInt16());
                int nWidth = (int)(br.ReadInt16());
                int nHeight = (int)(br.ReadInt16());
                int nByteLength = (int)br.ReadInt32();
                rectUpdates.Add(new Rectangle(nLeft, nTop, nWidth, nHeight));
                // This is the compressed byte length
                rgByteLengths.Add(nByteLength);
              }

              for (int nUpdate = 0; nUpdate < nUpdates; nUpdate++)
              {
                Rectangle rect = rectUpdates[nUpdate];
                byte[] rgbUpdateData = new byte[rgByteLengths[nUpdate] + 256];
                Array.Copy(br.ReadBytes(rgByteLengths[nUpdate]), rgbUpdateData, rgByteLengths[nUpdate]);
                ScreenUpdateData screenUpdate = new ScreenUpdateData(
                  rect,
                  rgbUpdateData,
                  ByteBufferContentType.CompressedDifferenceMap,
                  rgByteLengths[nUpdate]
                  );

                // decompress the packet 
                screenUpdate.SetByteContentType(ByteBufferContentType.UncompressedDifferenceMap);
                listOfScreenUpdates.Add(screenUpdate);
              }

              br.Close();
              screenUpdatePacket = new ScreenUpdateDataPacket(listOfScreenUpdates);
            }
            break;
          }
        }
      }

      return screenUpdatePacket;
    }

    public virtual CommunicationsResponse BeginWorkerThreads() { return CommunicationsResponse.NotYetImplemented; }

    public virtual CommunicationsResponse BroadcastScreenUpdateNotification(
      ScreenUpdateDataPacket screenUpdateDataPacket, int nMaxSize
      ) { return CommunicationsResponse.NotYetImplemented; }

    public virtual CommunicationsResponse BroadcastScreenResolutionChangeOrZoom(int nWidth, int nHeight)
    {
      CommunicationsResponse resp = CommunicationsResponse.Success;
      if (ConnectionHub != null)
      {
        using (MemoryStream msOutgoingPacket = new MemoryStream(256))
        {
          using (BinaryWriter bw = new BinaryWriter(msOutgoingPacket))
          {
            // 2 represents a screen size change
            bw.Write((short)MessageTypes.HostResolutionChange);
            bw.Write((short)Convert.ToInt16(nWidth));
            bw.Write((short)Convert.ToInt16(nHeight));
            bw.BaseStream.Seek(0, SeekOrigin.Begin);
            int srcOrigLen = (int)bw.BaseStream.Length;

            byte[] rgMessageToBroadcast =
              CommConnection.CreateCommandPacket(
                msOutgoingPacket.GetBuffer(),
                srcOrigLen);
            if (ConnectionHub != null)
            {
              if (this.GetType() == typeof(P2PCommunicationsLibrary)) { BroadcastDataOut(rgMessageToBroadcast, rgMessageToBroadcast.Length); }
              else { ConnectionHub.EnqueueForSend(rgMessageToBroadcast); }
            }
            bw.Close();
          }
        }
      }
      return resp;
    }

    public virtual CommunicationsResponse BroadcastCommandsToExecute(MemoryStream commandsToExe)
    {
      if (ConnectionHub != null)
      {
        int nLenCmdStream = (int)commandsToExe.Length;
        using (MemoryStream msOutgoingPacket = new MemoryStream(nLenCmdStream))
        {
          using (BinaryWriter bw = new BinaryWriter(msOutgoingPacket))
          {
            // 2 represents a screen size change
            bw.Write((short)MessageTypes.CommandExecutionPacket);
            bw.Write(commandsToExe.GetBuffer(), 0, nLenCmdStream);
            bw.BaseStream.Seek(0, SeekOrigin.Begin);
            int srcOrigLen = (int)bw.BaseStream.Length;

            byte[] rgMessageToBroadcast =
              CommConnection.CreateCommandPacket(
                msOutgoingPacket.GetBuffer(),
                srcOrigLen);

            if (this.GetType() == typeof(P2PCommunicationsLibrary))
            {
              BroadcastDataOut(rgMessageToBroadcast,
              srcOrigLen + CommConnection.nPacketHeaderSize + CommConnection.nPacketLengthPrefixSize);
            }
            else
            {
              while (ConnectionHub != null && ConnectionHub.SendMessageOut(
                rgMessageToBroadcast,
                srcOrigLen + CommConnection.nPacketHeaderSize + CommConnection.nPacketLengthPrefixSize,
                CommConnection.eMessagePriority.High) == false)
              {
                Thread.Sleep(5);
                Application.DoEvents();
              }
            }

            bw.Close();
          }
        }
      }
      return CommunicationsResponse.Success;
    }

    public virtual CommunicationsResponse BroadcastDataOutDirect(
  byte[] rgBytesToSend, int nLen
  ) { return CommunicationsResponse.SupportedByHTMLOnly; }

    public virtual CommunicationsResponse BroadcastDataOut(
      byte[] rgBytesToSend, int nLen
      ) { return CommunicationsResponse.NotYetImplemented; }

    public virtual CommunicationsResponse EstablishOutgoingConnection(
      string sRemoteHost, int nPort, SupportedCommunicationsProtocol Protocol
      ) { return CommunicationsResponse.NotYetImplemented; }

    public virtual CommunicationsResponse PollForNewDataFromServer()
    { return CommunicationsResponse.Error; }

    public virtual CommunicationsResponse AcceptIncomingConnections(
      IPAddress localIP, int nPort
      ) { return CommunicationsResponse.SupportedByP2POnly; }

    public int UserIdentity
    {
      get { return nUserIdentity; }
      set { nUserIdentity = value; }
    }

    public string UserAuthenticationToken
    {
      get { return sUserAuthenticationToken; }
      set { sUserAuthenticationToken = value; }
    }

    public string UserName
    {
      get { return sUserName; }
      set { sUserName = value; }
    }
  }

  public enum CommunicationsResponse
  {
    Error = 0,
    Success = 1,
    NotConnected = 2,
    BroadcastQueueFull = 4,
    NotYetImplemented = 8,
    SupportedByP2POnly = 16,
    SupportedByHTMLOnly = 32,
    NoDataAvailable = 64,
    ServerNotAvailable = 128,
    BroadcastFullImageInstead = 256,
    BroadcastQueueWasFull_SlowDown = 512,
    BroadcastQueueWasFull_SlowDown_And_Rebroadcast = 1024,
    AskToPresent = 2048,
    AcceptedPresent = 4096,
    RejectedPresent = 8192
  }

  /// <summary>
  /// All communications layers must implement these methods (although many are taken care of when
  /// inheriting from the CommunicationsLibrary base class
  /// </summary>
  public interface ICommunications
  {
    CommunicationsResponse BeginWorkerThreads();
    CommunicationsResponse BroadcastScreenUpdateNotification(ScreenUpdateDataPacket screenUpdateDataPacket, int nMaxUpdateSize);
    CommunicationsResponse BroadcastDataOut(byte[] rgBytesToSend, int nLen);
    CommunicationsResponse EstablishOutgoingConnection(string sRemoteHost, int nPort, SupportedCommunicationsProtocol Protocol);
    CommunicationsResponse AcceptIncomingConnections(IPAddress localIP, int nPort);
    CommunicationsResponse BroadcastScreenResolutionChangeOrZoom(int nWidth, int nHeight);
    CommunicationsResponse PollForNewDataFromServer();
    CommunicationsResponse BroadcastDataOutDirect(byte[] rgBytesToSend, int nLen);
    void WriteoutgoingData(int nMessageSize, byte[] rgBytes, int nLen);
    void WriteIncomingData(int nMessageSize, byte[] rgBytes, int nLen);
    byte[] GetPlaybackData();
    bool PlayingBackData { get;}
    void EnqueuePlaybackData(byte[] rgPlaybackData);
    void SetHostNegotiationStatus(HostNegotiationStatus status);
    void SetHostResponseStatus(HostNegotiationStatus status);
  }
}

